從 Angular 17 開始,團隊開始將查詢裝飾器 (query decorators
) 遷移到 signal
。此後以下查詢裝飾器已遷移。
@ViewChild -> viewchild()
@ViewChildren -> viewChildren()
@contentChild -> contentChild()
@contentChildren -> contentChildren()
今天,我將介紹 viewChildren
,它可以透過範本變數 (template variables)、 directives 或組件類型 (component type) 進行查詢。
ViewChildren
裝飾器傳回一個 QueryList
,而 viewChildren
函數傳回一個 Signal<ReadT[]>
。viewChildren
函數的第二個參數是具有 read
屬性的選項物件。然後,應用程式檢索相同類型的元素。@for (i of [0, 1, 2, 3, 4, 5, 6, 7]; track $index) {
<button #el (click)="updateVariableClicked(i)">{{ i + 1 }}</button>
}
els = viewChildren('el', { read: ElementRef<HTMLButtonElement[]> });
constructor() {
console.log('refs', this.els().length); // 0
}
ngAfterViewInit(): void {
console.log('ngAfterViewInit, refs ->', this.els().length); // 8
}
ngOnInit(): void {
console.log('ngOnInit, refs ->', this.els().length); // 0
}
viewChildren
函數在 AfterViewInit
執行後可用。 在 constructor
和 ngOnInit
中,元素數量為0。 在 ngAfterViewInit
中,元素數量為 8。
在以下例子中,我將展示 viewChildren
如何透過範本變數 (template variables)、directives 和 Angular 組件進行查詢。
import { Component, ElementRef, signal, viewChildren } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
@Component({
selector: 'app-query-by-variable',
standalone: true,
imports: [NgTemplateOutlet],
template: `
<div class="container">
<div>
<h3>Query by the template variables</h3>
<p>Select a template</p>
@for (i of [0, 1, 2, 3, 4, 5, 6, 7]; track $index) {
<button #el (click)="updateVariableClicked(i)">{{ i + 1 }}</button>
}
</div>
<ng-container *ngTemplateOutlet="t; context: { $implicit: variableBtn() }" />
</div>
<ng-template #t let-id>
<p>Simple Template {{ id }}</p>
</ng-template>
`,
})
export class AppQueryByVariableComponent {
variableBtn = signal(1);
els = viewChildren('el', { read: ElementRef<HTMLButtonElement[]> });
updateVariableClicked(index: number) {
const prevValue = this.variableBtn();
this.variableBtn.set(index + 1);
const styles = this.els().map((e) => e.nativeElement.style);
styles[prevValue - 1].backgroundColor = 'rgb(239, 239, 239)'
styles[index].backgroundColor = 'goldenrod';
}
}
els = viewChildren('el', { read: ElementRef<HTMLButtonElement[]> });
viewChildren
函數透過範本變數 #el
查詢按鈕清單。選擇器是 el
,第二個參數 { read: ElementRef<HTMLButtonElement[]> }
確保檢索 ElementRef 陣列。
updateVariableClicked(index: number) {
const prevValue = this.variableBtn();
this.variableBtn.set(index + 1);
const styles = this.els().map((e) => e.nativeElement.style);
styles[prevValue - 1].backgroundColor = 'rgb(239, 239, 239)';
styles[prevValue - 1].borderStyle = 'solid';
styles[prevValue - 1].borderWidth = '1px';
styles[index].backgroundColor = 'goldenrod';
styles[index].borderStyle = 'dashed';
styles[index].borderWidth = '0.25rem';
}
當使用者點擊按鈕時,將執行 updateVariableClicked
方法來更新 variableBtn
signal。 ngtTemplate
context 接收 variableBtn
值並在範本中顯示該數字。 此外,先前選擇的按鈕會刪除背景顏色,並且所選按鈕的背景顏色會更新為金黃色。
import { Directive, ElementRef, inject } from "@angular/core";
@Directive({
selector: 'button[appSelectTemplate]',
standalone: true,
})
export class AppSelectTemplateDirective {
style = inject<ElementRef<HTMLButtonElement>>(ElementRef).nativeElement.style;
changeBackgroundColor(color = 'rgb(239, 239, 239)') {
const isRevert = color === 'rgb(239, 239, 239)';
this.style.backgroundColor = color;
this.style.borderWidth = isRevert ? '1px' : '0.25rem';
this.style.borderStyle = isRevert ? 'solid' : 'dashed';
}
}
AppSelectTemplateDirective
directive提供了一種更新按鈕的背景顏色、邊框寬度和邊框樣式的方法。當選擇按鈕時,邊框寬度增加到 0.25rem,邊框樣式為虛線。當取消選擇按鈕時,邊框寬度和按鈕樣式分別恢復為 1px 和實心。
import { Component, signal, viewChildren } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { AppSelectTemplateDirective } from './select-template.directive';
@Component({
selector: 'app-query-by-directive',
standalone: true,
imports: [NgTemplateOutlet, AppSelectTemplateDirective],
template: `
<div class="container">
<div>
<h3>Query by the type of directive</h3>
<p>Click a button to select a template</p>
@for (i of [0, 1, 2, 3]; track $index) {
<button appSelectTemplate (click)="updateLastClicked(i)">{{ i + 1 }}</button>
}
</div>
<ng-container *ngTemplateOutlet="t; context: { $implicit: lastClickedBtn() }" />
</div>
<ng-template #t let-id>
<p>Simple Template {{ id }}</p>
</ng-template>`,
})
export class AppQueryByDirectiveComponent {
lastClickedBtn = signal(1);
selectTemplateDirectives = viewChildren(AppSelectTemplateDirective);
updateLastClicked(index: number) {
const prevValue = this.lastClickedBtn();
const directives = this.selectTemplateDirectives();
directives[prevValue - 1].changeBackgroundColor();
directives[index].changeBackgroundColor('cyan');
this.lastClickedBtn.set(index + 1);
}
}
<button appSelectTemplate (click)="updateLastClicked(i)">{{ i + 1 }}</button>
AppQueryByDirectiveComponent
組件指定按鈕的 appSelectLabel
屬性。
selectedDirectives = viewChildren(AppSelectTemplateDirective);
viewChildren
函數查詢 AppSelectTemplateDirective
directives。當使用者按一下該按鈕時,將執行 updateLastClicked
方法。
updateLastClicked(index: number) {
const prevValue = this.lastClickedBtn();
const directives = this.selectedDirectives();
directives[prevValue - 1].changeBackgroundColor();
directives[index].changeBackgroundColor('cyan');
this.lastClickedBtn.set(index + 1);
}
此方法恢復前一個按鈕的樣式; 所選按鈕具有青色背景、0.25rem 邊框寬度和虛線。
import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
const imgURL = 'https://picsum.photos/300/200';
@Component({
selector: 'app-photo',
standalone: true,
template: `
<div class="photo">
<img [src]="img()" alt="Random picture" />
</div>
`,
})
export default class PhotoComponent {
random = signal(Date.now());
img = computed(() => `${imgURL}?random=${this.random()}`)
}
PhotoComponent
組件有一個 signal 來定義圖片 URL 的隨機種子。 當任何 signal value 更新時,img computed signal 會產生新的圖像 URL。
import { ChangeDetectionStrategy, Component, viewChildren } from '@angular/core';
import AppPhotoComponent from './query-component/photo.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [AppPhotoComponent],
template: `
<h3>Query by components</h3>
<div class="photos">
@let photos = photoComponents();
<div class="card">
<app-photo class="photo" />
<button (click)="changeImage(photos[0])">Change photo 1</button>
</div>
<div class="card">
<app-photo class="photo" />
<button (click)="changeImage(photos[1])">Change photo 2</button>
</div>
<div>
<app-photo class="photo" />
<button (click)="changeImage(photos[2])">Change photo 3</button>
</div>
</div>
`,
})
export class App {
name = 'iTHome Ironman 2024 day 20';
description = 'Introduction to viewChildren';
photoComponents = viewChildren(AppPhotoComponent);
changeImage(photo: AppPhotoComponent) {
photo.random.set(Date.now());
}
}
App
組件使用 viewChildren
函數查詢 PhotoComponent
。 App
組件存取 photoComponents
變數 以將 photo
遞給 changeImage
方法。當使用者按一下該按鈕時,該組件會顯示一個新圖像。
viewChildren
可以查詢 elements、directives 和 components。第一個參數是一個 selector,它是範本變數或類型。read
屬性指定 viewChildren
傳回的元素類型。viewChildren
函數傳回 signal, 值是一個 array 。當函數找不到匹配項時,它會傳回一個 empty array,而不是錯誤。viewChildren
在 AfterViewInit hook 之後可用。鐵人賽第 20 天就這樣結束了。